
require("vector")

local brickSizeTU = vector{0.5, 0.5, 0.2}
local brickSizeBG = vector{1, 1, 1}
local mazeCenterTU = vector{0, 0, 32}
local baseZoneHeightTU = 10
local noDiagonalConnections = false
local printPlantErrors = true

local colorFx = {
	none = 0,
	pearl = 1,
	chrome = 2,
	glow= 3,
	blink = 4,
	swirl = 5,
	rainbow = 6,
}
local shapeFx = {
	none = 0,
	undulo = 1,
	water = 2,
}
local angleIds = {
	north = 0,
	east = 1,
	south = 2,
	west = 3,
	up = 4,
	down = 5,
	plusy = 0,
	plusx = 1,
	minusy = 2,
	minusx = 3,
	plusz = 4,
	minusz = 5,
}

--[[
brick = see getAllBricks
template brick = {
	brickId
	color
	colorFx
	shapeFx
	posRelTU (position relative to center of template box)
	angleId
	uiName
	ntName
	sizeBG
	loadStrings (table of angle id -> load string after angle id)
}
connection = {
	posRelC (vector, TU of center of connector cube, relative to center of box)
	color
}
template = {
	type = "template" or "prop"
	color
	colorFx
	weight
	boxSizeC - if template
	boxSizeBG - if prop
	bricks (list of template bricks)
	connections (list of connections, if template)
	rotationAngleId
}
template string =
	color
	colorFx
	box size x, y, z in cells
	connections sorted by increasing x, increasing y, increasing z
		color
		angleId
		position x, y, z in half-cells relative to center
	i.e. 3,0,1,1,1,(3,1,1,0,0),(3,3,-1,0,0)
prop template string =
	color
	colorFx
	box size x in bricks
	box size y in bricks
	angleId
	i.e. 3,0,4,4,0
]]

----
-- Utility functions

local function talk(s)
	s = tostring(s)
	print(s)
	ts.call("talk", s)
end

local function wideset(t, v, ...)
	local keys = {...}; local nk = #keys; local lk = keys[nk];
	for i = 1, nk-1 do local k = keys[i];
		if not t[k] then t[k] = {} end; t = t[k];
	end
	t[lk] = v;
end
local function wideget(t, ...)
	local keys = {...}; local nk = #keys;
	for i = 1, nk do t = t[keys[i]]; if t==nil then return nil end; end
	return t
end
local function split(s, sep, regex)
	if type(s)~="string" then error("string.split: expected string at argument 1, got "..type(s)) end
	if sep==nil or sep=="" then
		local t = {}
		local ns = #s
		for x = 1, ns do
			table.insert(t, s:sub(x, x))
		end
		return t
	elseif type(sep)=="string" then
		local t = {}
		if #s>0 then
			local first = 1
			while true do
				local last, newfirst = s:find(sep, first, regex)
				if not last then break end
				table.insert(t, s:sub(first, last-1))
				first = newfirst+1
			end
			table.insert(t, s:sub(first, #s))
		end
		return t
	else
		error("string.split: expected nil or string at argument 2, got "..type(sep))
	end
end
local function map(t, f) local u = {}; for i, v in ipairs(t) do u[i] = f(v) end; return u; end
local function vectorfromstr(s) return map(split(s, " "), tonumber) end
local function vectostr(v) return table.concat(v, " ") end

----
-- Template generation

local function copyTemplateBrick(tempBrick)
	local newTempBrick = {
		brickId  = tempBrick.brickId,
		color    = tempBrick.color,
		colorFx  = tempBrick.colorFx,
		shapeFx  = tempBrick.shapeFx,
		posRelTU = tempBrick.posRelTU,
		angleId  = tempBrick.angleId,
		uiName   = tempBrick.uiName,
		ntName   = tempBrick.ntName,
		symmetry = tempBrick.symmetry,
		sizeBG   = tempBrick.sizeBG,
	}
	return newTempBrick
end
local function copyConnection(conn)
	local newconn = {
		posRelC = conn.posRelC,
		color = conn.color,
		colorFx = conn.colorFx,
	}
	return newconn
end

local function rotateVector(vec, rot)
	rot = rot%4
	local vec2
	if     rot==0 then vec2 = vector{  vec[1],  vec[2], vec[3] }
	elseif rot==1 then vec2 = vector{  vec[2], -vec[1], vec[3] }
	elseif rot==2 then vec2 = vector{ -vec[1], -vec[2], vec[3] }
	elseif rot==3 then vec2 = vector{ -vec[2],  vec[1], vec[3] }
	else error("invalid rotation "..rot) end
	return vec2
end
local function rotateSizeVector(vec, rot)
	rot = rot%2
	local vec2
	if     rot==0 then vec2 = vector{ vec[1], vec[2], vec[3] }
	elseif rot==1 then vec2 = vector{ vec[2], vec[1], vec[3] }
	else error("invalid rotation "..rot) end
	return vec2
end

local function getBoxBottomCenter(box)
	return vector {
		(box[1].x+box[2].x)/2,
		(box[1].y+box[2].y)/2,
		box[1].z
	}
end
local function getBoxCenter(box)
	return (box[1]+box[2])/2
end

local function posIsInBox(box, pos)
	return
		pos[1] >= box[1][1] and
		pos[2] >= box[1][2] and
		pos[3] >= box[1][3] and
		pos[1] <= box[2][1] and
		pos[2] <= box[2][2] and
		pos[3] <= box[2][3]
end

local function brickIsPropTemplateMarker(brick)
	return brick.uiName == "2x2F Print" and brick.ntName and brick.ntName:find("^prop_")
end

local function brickIsTemplateMarker(brick)
	return brick.uiName == "2x2F Print" and brick.ntName and brick.ntName:find("^template")
end

local function getBricksInBox(allBricks, boxAbsTU)
	local bricks = {}
	for _, brick in ipairs(allBricks) do
		if posIsInBox(boxAbsTU, brick.posAbsTU) then
			if brickIsPropTemplateMarker(brick) or brickIsTemplateMarker(brick) then
				talk("Warning: Template marker inside other template at "..tostring(brick.posAbsTU))
			else
				table.insert(bricks, brick)
			end
		end
		coroutine.yield(10)
	end
	return bricks
end

local function getTempBricksInBox(allBricks, boxAbsTU, boxCenterAbsTU)
	local tempBricks = {}
	
	for _, brick in ipairs(allBricks) do
		if posIsInBox(boxAbsTU, brick.posAbsTU) then
			local newTempBrick = copyTemplateBrick(brick)
			newTempBrick.posRelTU = brick.posAbsTU - boxCenterAbsTU
			newTempBrick.posAbsTU = nil
			table.insert(tempBricks, newTempBrick)
		end
		coroutine.yield(10)
	end
	return tempBricks
end

local function computeMarkerBoxTU(boxSizeTU, markerPosAbsTU, markerAngleId)
	local rlow = vector{-0.5, 0.5, -0.1} -- half marker size TU
	local rhigh = rlow + boxSizeTU
	local low  = rotateVector(rlow , markerAngleId) + markerPosAbsTU
	local high = rotateVector(rhigh, markerAngleId) + markerPosAbsTU
	local boxAbsTU = {
		vector{
			math.min(low[1], high[1]),
			math.min(low[2], high[2]),
			math.min(low[3], high[3]),
		},
		vector{
			math.max(low[1], high[1]),
			math.max(low[2], high[2]),
			math.max(low[3], high[3]),
		},
	}
	return boxAbsTU
end

local function brickIsConnector(brick, connectorUiName)
	return brick.uiName == connectorUiName
end

local function getBrickAtPosition(allBricks, posAbsTU)
	for _, brick in ipairs(allBricks) do
		if brick.posAbsTU == posAbsTU then
			return brick
		end
		coroutine.yield(10)
	end
	return nil
end

local function getConnectorBrickAtPosition(allBricks, posAbsTU, connectorUiName)
	local brick = getBrickAtPosition(allBricks, posAbsTU)
	if brick and brickIsConnector(brick, connectorUiName) then return brick else return nil end
end

local function connectionGetString(conn)
	return string.format("%i,%i,%i",
		conn.posRelC.x*2,
		conn.posRelC.y*2,
		conn.posRelC.z*2
	)
end

local function checkConnectorAtPosition(allBricks, conns, boxCenterAbsTU, posAbsTU, cellSizeTU, connectorUiName)
	local brick = getConnectorBrickAtPosition(allBricks, posAbsTU, connectorUiName)
	if brick then
		local conn = {
			posRelC = (posAbsTU - boxCenterAbsTU) / cellSizeTU,
			color = brick.color,
			colorFx = brick.colorFx,
		}
		conn.connectionString = connectionGetString(conn)
		if conns[conn.connectionString] ~= nil then
			talk("Error: Multiple connectors at "..tostring(posAbsTU).." (string "..conn.connectionString..")")
			return
		end
		conns[conn.connectionString] = conn
	end
end

local function getGridCenteredBox(boxCenterAbsTU, boxSizeTU, cellSizeTU)
	local cboxLAbsTU = boxCenterAbsTU - boxSizeTU/2 + cellSizeTU/2
	local cboxHAbsTU = boxCenterAbsTU + boxSizeTU/2 - cellSizeTU/2
	local cbox = {cboxLAbsTU, cboxHAbsTU}
	return cbox
end

local function getConnectionsAroundBoxTU(allBricks, boxCenterAbsTU, boxSizeTU, cellSizeTU, connectorUiName)
	local conns = {}
	local cbox = getGridCenteredBox(boxCenterAbsTU, boxSizeTU, cellSizeTU)
	
	for x = cbox[1].x - cellSizeTU.x, cbox[2].x + cellSizeTU.x, cellSizeTU.x do
	for y = cbox[1].y - cellSizeTU.y, cbox[2].y + cellSizeTU.y, cellSizeTU.y do
	for z = cbox[1].z - cellSizeTU.z, cbox[2].z + cellSizeTU.z, cellSizeTU.z do
		local posAbsTU = vector{x, y, z}
		if not posIsInBox(cbox, posAbsTU) then
			checkConnectorAtPosition(allBricks, conns, boxCenterAbsTU, posAbsTU, cellSizeTU, connectorUiName)
		end
	end
	end
	end
	
	return conns
end

local function getSizeWeightFromName(name)
	local sizexs, sizeys, sizezs, weights = name:match("^[a-z]+_([0-9]+)x([0-9]+)x([0-9]+)_?([0-9]*)$")
	if not (sizexs and sizeys and sizezs) then
		talk("Invalid marker brick name: "..name)
		return nil
	end
	local size = vector{tonumber(sizexs), tonumber(sizeys), tonumber(sizezs)}
	local weight = tonumber(weights) or 100
	return size, weight
end

local function boxGetVertices(box)
	local verts = {
		vector{box[1].x, box[1].y, box[1].z},
		vector{box[1].x, box[1].y, box[2].z},
		vector{box[1].x, box[2].y, box[1].z},
		vector{box[1].x, box[2].y, box[2].z},
		vector{box[2].x, box[1].y, box[1].z},
		vector{box[2].x, box[1].y, box[2].z},
		vector{box[2].x, box[2].y, box[1].z},
		vector{box[2].x, box[2].y, box[2].z},
	}
	return verts
end

local function boxGetLines(box)
	local verts = boxGetVertices(box)
	local lines = {
		{verts[1], verts[5]},
		{verts[5], verts[7]},
		{verts[7], verts[3]},
		{verts[3], verts[1]},
		{verts[2], verts[6]},
		{verts[6], verts[8]},
		{verts[8], verts[4]},
		{verts[4], verts[2]},
		{verts[1], verts[2]},
		{verts[3], verts[4]},
		{verts[5], verts[6]},
		{verts[7], verts[8]},
	}
	return lines
end

local function drawLine(line)
	local shape = ts.call("drawLine", vectostr(line[1]), vectostr(line[2]))
	table.insert(MazeData.shapes, shape)
end
local function clearShapes()
	for _, shape in ipairs(MazeData.shapes) do
		if ts.call("isObject", shape) == "1" then
			ts.callobj(shape, "delete")
		end
	end
	MazeData.shapes = {}
end

local function drawBox(boxAbsTU)
	local linesAbsTU = boxGetLines(boxAbsTU)
	for lineidx, line in ipairs(linesAbsTU) do
		drawLine(line)
	end
end

local function getBoxSize(box)
	return box[2]-box[1]
end

local function constructTemplateFromMarker(marker, allBricks, cellSizeTU, connectorUiName)
	local size, weight = getSizeWeightFromName(marker.ntName)
	if not size then return nil end
	
	local boxSizeCunrotated = size
	local boxSizeTUunrotated = boxSizeCunrotated * cellSizeTU
	local boxAbsTU = computeMarkerBoxTU(boxSizeTUunrotated, marker.posAbsTU, marker.angleId)
	local boxCenterAbsTU = getBoxCenter(boxAbsTU)
	local boxSizeTU = getBoxSize(boxAbsTU)
	local boxSizeC = boxSizeTU / cellSizeTU
	
	--drawBox(boxAbsTU)
	
	local tempBricks = getTempBricksInBox(allBricks, boxAbsTU, boxCenterAbsTU)
	local conns = getConnectionsAroundBoxTU(allBricks, boxCenterAbsTU, boxSizeTU, cellSizeTU, connectorUiName)
	
	local template = {
		type = "template",
		color = marker.color,
		colorFx = marker.colorFx,
		weight = weight,
		boxSizeC = boxSizeC,
		bricks = tempBricks,
		connections = conns,
		rotationAngleId = 0,
	}
	return template
end

local function templateToString(temp)
	local connStrings = {}
	if temp.type == "template" then
		for connString, conn in pairs(temp.connections) do
			table.insert(connStrings, connString)
		end
	end
	return string.format("(type=%s color=%i colorfx=%i size=%s #bricks=%s%s)",
		temp.type,
		temp.color,
		temp.colorFx,
		tostring(temp.type == "template" and temp.boxSizeC or temp.boxSizeBG),
		temp.bricks and ""..#temp.bricks or "N/A",
		temp.type == "template" and " conns="..table.concat(connStrings, " ") or ""
	)
end

local function constructPropTemplateFromMarker(marker, allBricks)
	local size, weight = getSizeWeightFromName(marker.ntName)
	if not size then return nil end
	
	local boxSizeBGunrotated = size
	local boxSizeTUunrotated = boxSizeBGunrotated * brickSizeTU
	local boxAbsTU = computeMarkerBoxTU(boxSizeTUunrotated, marker.posAbsTU, marker.angleId)
	local boxBottomCenterAbsTU = getBoxBottomCenter(boxAbsTU)
	local boxSizeTU = getBoxSize(boxAbsTU)
	local boxSizeBG = boxSizeTU / brickSizeTU
	
	--drawBox(boxAbsTU)
	
	local tempBricks = getTempBricksInBox(allBricks, boxAbsTU, boxBottomCenterAbsTU)
	
	local template = {
		type = "prop",
		color = marker.color,
		colorFx = marker.colorFx,
		weight = weight,
		boxSizeBG = boxSizeBG,
		bricks = tempBricks,
		rotationAngleId = marker.angleId,
	}
	
	return template
end

local function tempBrickIsPropSpawn(brick)
	return
		brick.sizeBG.z==1 and
		brick.ntName and brick.ntName == "prop" and
		(brick.uiName=="1x1F Print" or brick.uiName=="1x2F Print" or brick.uiName=="2x2F Print")
end

local function rotateTemplate(template, rotationAngleId)
	local newBricks = {}
	for _, brick in ipairs(template.bricks) do
		local newBrick = copyTemplateBrick(brick)
		newBrick.posRelTU = rotateVector(brick.posRelTU, rotationAngleId)
		newBrick.sizeBG = rotateSizeVector(brick.sizeBG, rotationAngleId)
		newBrick.angleId = (brick.angleId + rotationAngleId) % 4
		table.insert(newBricks, newBrick)
	end
	
	local newConns
	if template.type == "template" then
		newConns = {}
		for connString, conn in pairs(template.connections) do
			local newConn = copyConnection(conn)
			newConn.posRelC = rotateVector(conn.posRelC, rotationAngleId)
			
			newConn.connectionString = connectionGetString(newConn)
			assert(newConns[newConn.connectionString] == nil)
			newConns[newConn.connectionString] = newConn
		end
	end
	
	local template2 = {
		type = template.type,
		color = template.color,
		colorFx = template.colorFx,
		weight = template.weight,
		boxSizeC  = template.boxSizeC  and rotateSizeVector(template.boxSizeC , rotationAngleId),
		boxSizeBG = template.boxSizeBG and rotateSizeVector(template.boxSizeBG, rotationAngleId),
		bricks = newBricks,
		connections = newConns,
		rotationAngleId = (template.rotationAngleId + rotationAngleId) % 4,
	}
	
	return template2
end

local function insertTemplateIntoList(allTemplates, template)
	table.insert(allTemplates, template)
	if template.connections then
		for connString, conn in pairs(template.connections) do
		end
	end
end

local function extractTemplatesFromBricks(allBricks, cellSizeTU, connectorUiName)
	local allTemplates = {}
	
	for _, brick in ipairs(allBricks) do
		local template1
		if brickIsTemplateMarker(brick) then
			template1 = constructTemplateFromMarker(brick, allBricks, cellSizeTU, connectorUiName)
		elseif brickIsPropTemplateMarker(brick) then
			template1 = constructPropTemplateFromMarker(brick, allBricks)
		end
		if template1 then
			local template2 = rotateTemplate(template1, 1)
			local template3 = rotateTemplate(template1, 2)
			local template4 = rotateTemplate(template1, 3)
			
			insertTemplateIntoList(allTemplates, template1)
			insertTemplateIntoList(allTemplates, template2)
			insertTemplateIntoList(allTemplates, template3)
			insertTemplateIntoList(allTemplates, template4)
		end
		
		coroutine.yield(10)
	end
	
	return allTemplates
end

----
-- Template placement by reading mini map

local function getBrickGridCenteredBoxBG(brick)
	local boxCenterAbsBG = brick.posAbsBG
	local boxSizeBG = brick.sizeBG
	local cboxLBG = boxCenterAbsBG - boxSizeBG/2 + brickSizeBG/2
	local cboxHBG = boxCenterAbsBG + boxSizeBG/2 - brickSizeBG/2
	local cbox = {cboxLBG, cboxHBG}
	return cbox
end

local function brickGridPosStr(posAbsBG)
	local pos = posAbsBG * 2
	local str = string.format("%i,%i,%i", pos.x+0.001, pos.y+0.001, pos.z+0.001)
	return str
end
local function brickGridGet(brickGrid, posAbsBG)
	local str = brickGridPosStr(posAbsBG)
	return brickGrid[str]
end
local function brickGridSet(brickGrid, posAbsBG, brick)
	local str = brickGridPosStr(posAbsBG)
	assert(not brickGrid[str])
	brickGrid[str] = brick
end

local function checkBrickConnectionAtPositionBG(brickGrid, conns, boxCenterAbsBG, posAbsBG, angleId)
	local connBrick = brickGridGet(brickGrid, posAbsBG)
	if connBrick then
		local conn = {
			color = connBrick.color,
			posRelC = posAbsBG - boxCenterAbsBG,
		}
		conn.connectionString = connectionGetString(conn)
		assert(conns[conn.connectionString] == nil)
		conns[conn.connectionString] = conn
	end
end

local function getConnectionsToBrick(brickGrid, brick)
	local conns = {}
	local cbox = getBrickGridCenteredBoxBG(brick)
	local boxCenterAbsBG = brick.posAbsBG
	
	if noDiagonalConnections then
		-- north face (+Y) and south face (-Y)
		local yh = cbox[2].y + 1
		local yl = cbox[1].y - 1
		for x = cbox[1].x, cbox[2].x do
		for z = cbox[1].z, cbox[2].z do
			checkBrickConnectionAtPositionBG(brickGrid, conns, boxCenterAbsBG, vector{x, yh, z}, angleIds.north)
			checkBrickConnectionAtPositionBG(brickGrid, conns, boxCenterAbsBG, vector{x, yl, z}, angleIds.south)
		end
		end
		
		-- east face (+X) and west face (-X)
		local xh = cbox[2].x + 1
		local xl = cbox[1].x - 1
		for y = cbox[1].y, cbox[2].y do
		for z = cbox[1].z, cbox[2].z do
			checkBrickConnectionAtPositionBG(brickGrid, conns, boxCenterAbsBG, vector{xh, y, z}, angleIds.east )
			checkBrickConnectionAtPositionBG(brickGrid, conns, boxCenterAbsBG, vector{xl, y, z}, angleIds.west )
		end
		end
		
		-- up face (+Z) and down face (-Z)
		local zh = cbox[2].z + 1
		local zl = cbox[1].z - 1
		for x = cbox[1].x, cbox[2].x do
		for y = cbox[1].y, cbox[2].y do
			checkBrickConnectionAtPositionBG(brickGrid, conns, boxCenterAbsBG, vector{x, y, zh}, angleIds.up   )
			checkBrickConnectionAtPositionBG(brickGrid, conns, boxCenterAbsBG, vector{x, y, zl}, angleIds.down )
		end
		end
	else
		for x = cbox[1].x - 1, cbox[2].x + 1, 1 do
		for y = cbox[1].y - 1, cbox[2].y + 1, 1 do
		for z = cbox[1].z - 1, cbox[2].z + 1, 1 do
			local posAbsBG = vector{x, y, z}
			if not posIsInBox(cbox, posAbsBG) then
				checkBrickConnectionAtPositionBG(brickGrid, conns, boxCenterAbsBG, posAbsBG)
			end
		end
		end
		end
	end
	
	return conns
end

local function getTemplateOfBrick(brickGrid, brick)
	local conns = getConnectionsToBrick(brickGrid, brick)
	
	local template = {
		type = "template",
		color = brick.color,
		colorFx = brick.colorFx,
		boxSizeC = brick.sizeBG,
		connections = conns,
	}
	return template
end

local function getTemplateOfTempPropSpawn(brick)
	local template = {
		type = "prop",
		color = brick.color,
		colorFx = brick.colorFx,
		shapeFx = brick.shapeFx,
		boxSizeBG = brick.sizeBG,
		rotationAngleId = brick.angleId,
	}
	return template
end

local function rotateLoadStringAfterAngleId(loadString, rotationAngleId)
	local rotatedLoadString = loadString:gsub("%+%-[^\n]+", function(line)
		local rotatedLine = ts.call("gls_transformLoadLine", line, rotationAngleId, "0 0 0");
		return rotatedLine
	end)
	return rotatedLoadString
end

local function getBrickLoadStrings(brickId, allBrickLoadStrings)
	local loadStrings = allBrickLoadStrings[brickId]
	if not loadStrings then
		local loadString = ts.callobj(brickId, "gls_getLoadString")
		local loadStringAfterAngleId = loadString:match("^[^\"]+\" [^ ]+ [^ ]+ [^ ]+ [^ ]+ (.+)$")
		assert(loadStringAfterAngleId, "Invalid load string for brick "..brickId..": \""..loadString.."\"")
		loadStrings = {
			[0] = rotateLoadStringAfterAngleId(loadStringAfterAngleId, 0),
			[1] = rotateLoadStringAfterAngleId(loadStringAfterAngleId, 1),
			[2] = rotateLoadStringAfterAngleId(loadStringAfterAngleId, 2),
			[3] = rotateLoadStringAfterAngleId(loadStringAfterAngleId, 3),
		}
		allBrickLoadStrings[brickId] = loadStrings
	end
	return loadStrings
end

local function getBrickLoadString(allBrickLoadStrings, brick, rotationAngleId)
	local loadStrings = getBrickLoadStrings(brick.brickId, allBrickLoadStrings)
	
	local loadString = string.format("%s\" %f %f %f %i %s",
		brick.uiName,
		brick.posAbsTU.x,
		brick.posAbsTU.y,
		brick.posAbsTU.z,
		brick.angleId % brick.symmetry,
		loadStrings[rotationAngleId]
	)
	return loadString
end

local function loadBrick(brick, rotationAngleId, loadedBricks, allBrickLoadStrings)
	table.insert(loadedBricks, getBrickLoadString(allBrickLoadStrings, brick, rotationAngleId))
end

local function getSeedFromPos(pos, seed)
	return pos.z*1000000 + pos.y*1000 + pos.x + seed
end

local function selectTemplateByWeight(templates, seed)
	local sum = 0
	for _, t in ipairs(templates) do
		sum = sum + t.weight
	end
	
	math.randomseed(seed)
	for i = 1, 10 do math.random() end
	local idx = math.random(1, sum)
	
	local cur = 0
	for _, t in ipairs(templates) do
		cur = cur + t.weight
		if cur >= idx then return t end
	end
	error("how")
end

local function templatesMatchConnections(t2, t1)
	for connString, conn in pairs(t1.connections) do
		local conn2 = t2.connections[connString]
		if not (
			(conn.colorFx == colorFx.none  and conn2 and conn2.color == conn.color) or
			(conn.colorFx == colorFx.blink and ((not conn2) or conn2.color == conn.color)) or
			(conn.colorFx == colorFx.swirl and (not conn2))
		) then
			return false
		end
	end
	return true
end

local function templatesMatch(t1, t2)
	if t1.type == t2.type and t1.color == t2.color then
		if t1.type == "template" then
			if
				t1.colorFx == t2.colorFx and
				t1.boxSizeC == t2.boxSizeC
			then
				return templatesMatchConnections(t1, t2)
			else
				return false
			end
		elseif t1.type == "prop" then
			return
				t1.colorFx == t2.colorFx and
				((t1.rotationAngleId == t2.rotationAngleId) or (t1.shapeFx == shapeFx.undulo or t2.shapeFx == shapeFx.undulo)) and
				(t1.boxSizeBG.x%2 == t2.boxSizeBG.x%2 and t1.boxSizeBG.y%2 == t2.boxSizeBG.y%2)
		end
	else
		
	end
end

local function getMatchingTemplates(allTemplates, template)
	local matchingTemplates = {}
	for _, template2 in ipairs(allTemplates) do
		if template2.type == template.type then
			if templatesMatch(template, template2) then
				table.insert(matchingTemplates, template2)
			end
		end
	end
	return matchingTemplates
end

local function loadTemplateAtPosTU(template, posAbsTU, loadedBricks, allBrickLoadStrings, allTemplates, seed)
	for _, tempBrick in ipairs(template.bricks) do
		if tempBrickIsPropSpawn(tempBrick) then
			local template = getTemplateOfTempPropSpawn(tempBrick)
			local templates = getMatchingTemplates(allTemplates, template)
			if #templates == 0 then
				talk("No template for prop spawn "..templateToString(template))
			else
				local tempPosAbsTU = tempBrick.posRelTU + posAbsTU - vector{0, 0, tempBrick.sizeBG.z * brickSizeTU.z /2}
				
				local tempSeed = getSeedFromPos(tempPosAbsTU, seed)
				local template = selectTemplateByWeight(templates, tempSeed)
				
				loadTemplateAtPosTU(template, tempPosAbsTU, loadedBricks, allBrickLoadStrings, allTemplates, seed + 1)
				
				coroutine.yield(10)
			end
		else
			local newTempBrick = copyTemplateBrick(tempBrick)
			newTempBrick.posRelTU = nil
			newTempBrick.posAbsTU = tempBrick.posRelTU + posAbsTU
			loadBrick(newTempBrick, template.rotationAngleId, loadedBricks, allBrickLoadStrings)
		end
	end
end

local function indicateOkBrick(brick)
	ts.callobj(brick.brickId, "setShapeFx", shapeFx.none)
end
local function indicateErrorBrick(brick)
	ts.callobj(brick.brickId, "setShapeFx", shapeFx.undulo)
end

local function translatePositionToReality(brickPosAbsTU, originTU, cellSizeTU)
	local offset = brickPosAbsTU - originTU
	local tempPosAbsTU = (offset * cellSizeTU / brickSizeTU) + mazeCenterTU
	return tempPosAbsTU
end

local function loadBrickToReality(allTemplates, brickGrid, brick, originTU, loadedBricks, allBrickLoadStrings, seed, cellSizeTU)
	local template = getTemplateOfBrick(brickGrid, brick)
	local templates = getMatchingTemplates(allTemplates, template)
	if #templates == 0 then
		indicateErrorBrick(brick)
	else
		indicateOkBrick(brick)
		
		local tempPosAbsTU = translatePositionToReality(brick.posAbsTU, originTU, cellSizeTU)
		
		local tempSeed = getSeedFromPos(tempPosAbsTU, seed)
		local template = selectTemplateByWeight(templates, tempSeed)
		
		loadTemplateAtPosTU(template, tempPosAbsTU, loadedBricks, allBrickLoadStrings, allTemplates, seed)
	end
end

local function getBrickGrid(bricks)
	local brickGrid = {}
	for _, brick in ipairs(bricks) do
		local boxBG = getBrickGridCenteredBoxBG(brick)
		
		for x = boxBG[1].x, boxBG[2].x do
		for y = boxBG[1].y, boxBG[2].y do
		for z = boxBG[1].z, boxBG[2].z do
			local posAbsBG = vector{x, y, z}
			assert(brickGridGet(brickGrid, posAbsBG)==nil, "multiple bricks at grid position "..x.." "..y.." "..z)
			brickGridSet(brickGrid, posAbsBG, brick)
		end
		end
		end
	end
	
	return brickGrid
end

local function loadBricksInBox(allTemplates, allBricks, boxAbsTU, loadedBricks, allBrickLoadStrings, seed, cellSizeTU)
	local bricksInBox = getBricksInBox(allBricks, boxAbsTU)
	local brickGrid = getBrickGrid(bricksInBox)
	local originTU = getBoxBottomCenter(boxAbsTU)
	
	for _, brick in ipairs(bricksInBox) do
		loadBrickToReality(allTemplates, brickGrid, brick, originTU, loadedBricks, allBrickLoadStrings, seed, cellSizeTU)
	end
end

local function getBrickBoxTU(brick)
	local boxAbsTU = {
		brick.posAbsTU - brick.sizeTU/2,
		brick.posAbsTU + brick.sizeTU/2,
	}
	return boxAbsTU
end

local function loadBricksOnBase(allTemplates, allBricks, baseBrick, loadedBricks, allBrickLoadStrings, seed, cellSizeTU)
	local brickBoxAbsTU = getBrickBoxTU(baseBrick)
	local baseBoxAbsTU = {
		brickBoxAbsTU[1] + vector{0, 0, baseBrick.sizeTU.z},
		brickBoxAbsTU[2] + vector{0, 0, baseZoneHeightTU},
	}
	loadBricksInBox(allTemplates, allBricks, baseBoxAbsTU, loadedBricks, allBrickLoadStrings, seed, cellSizeTU)
end

local function getBrickById(allBricks, id)
	return allBricks.byId[id]
end

local function getBrickByNtName(allBricks, ntName)
	return allBricks.byNtName[ntName] and allBricks.byNtName[ntName][1]
end

-- getting bricks from world

ts.eval[[
	function fxDtsBrickData::getUiName(%data) { return %data.uiName; }
	function fxDtsBrickData::getBrickSize(%data) { return %data.brickSizeX SPC %data.brickSizeY SPC %data.brickSizeZ; }
]]
local function getBrickData(brickObj)
	local data = ts.callobj(brickObj, "getDatablock")
	
	local angleId = tonumber(ts.callobj(brickObj, "getAngleId"))
	
	local posAbsTU = vector(vectorfromstr(ts.callobj(brickObj, "getPosition")))
	local posAbsBG = posAbsTU / brickSizeTU
	
	local sizeBG = rotateSizeVector(vector(vectorfromstr(ts.callobj(data, "getBrickSize"))), angleId)
	local sizeTU = sizeBG * brickSizeTU
	
	local ntName = ts.callobj(brickObj, "getName")
	ntName = ntName:sub(2, #ntName)
	if ntName=="" then ntName = nil end
	
	local brick = {
		brickId  = tonumber(brickObj),
		color    = tonumber(ts.callobj(brickObj, "getColorId")),
		colorFx  = tonumber(ts.callobj(brickObj, "getColorFxId")),
		shapeFx  = tonumber(ts.callobj(brickObj, "getShapeFxId")),
		posAbsTU = posAbsTU,
		angleId  = angleId,
		uiName   = ts.callobj(data, "getUiName"),
		ntName   = ntName,
		sizeBG   = sizeBG,
		sizeTU   = sizeTU,
		posAbsBG = posAbsBG,
		symmetry = tonumber(ts.callobj(data, "mm_getSymmetry")),
	}
	
	assert(brick.brickId )
	assert(brick.color   )
	assert(brick.colorFx )
	assert(brick.shapeFx )
	assert(brick.posAbsTU)
	assert(brick.angleId )
	assert(brick.uiName  )
	assert(brick.sizeBG  )
	assert(brick.sizeTU  )
	assert(brick.symmetry)
	
	return brick
end


local function getAllBricks()
	local allBricks = {
		byId = {},
		byNtName = {},
		-- is also a list
	}
	
	local mbgc = tonumber(ts.callobj("MainBrickGroup", "getCount"))
	for i = 1, mbgc do
		local bg = ts.callobj("MainBrickGroup", "getObject", i-1)
		local bgc = tonumber(ts.callobj(bg, "getCount"))
		for j = 1, bgc do
			local brickObj = ts.callobj(bg, "getObject", j-1)
			if ts.callobj(brickObj, "isDead")=="0" and ts.callobj(brickObj, "isPlanted")=="1" then
				local brick = getBrickData(brickObj)
				
				table.insert(allBricks, brick)
				
				allBricks.byId[brick.brickId] = brick
				
				if brick.ntName then
					local names = allBricks.byNtName[brick.ntName] or {}
					table.insert(names, brick)
					allBricks.byNtName[brick.ntName] = names
				end
			end
			
			coroutine.yield(10)
		end
	end
	
	return allBricks
end

local function createBrickFromLoadString(loadString)
	local brickId
	for line in loadString:gmatch("[^\r\n]+") do
		brickId = ts.call("gls_loadLoadStringLine", line)
	end
	brickId = tonumber(brickId)
	if brickId and brickId ~= 0 then
		ts.setobj(brickId, "generated", 1)
		ts.callobj(brickId, "onTemplateGenerated")
		return brickId
	else
		if printPlantErrors then talk("Error planting brick \""..(loadString:gsub("[\r\n]+$", "")).."\"") end
		return nil
	end
end

if (not exists and not MazeData) or (exists and not exists("MazeData")) then
	MazeData = {
		builtBricks = {},
		shapes = {},
	}
end
local function updateMazeBrickLoadStrings(newBrickLoadStrings)
	ts.call("gls_init")
	
	local newBricks = {}
	local builtBricks = MazeData.builtBricks
	
	for _, loadString in ipairs(newBrickLoadStrings) do
		newBricks[loadString] = true
	end
	
	-- delete old bricks that arent in the new list
	for loadString, brickId in pairs(builtBricks) do
		if not newBricks[loadString] then
			builtBricks[loadString] = nil
			if ts.call("isObject", brickId) == "1" then
				ts.callobj(brickId, "delete")
			end
			coroutine.yield(1)
		end
	end
	
	-- create new bricks that werent in the old list
	for _, loadString in ipairs(newBrickLoadStrings) do
		if (not builtBricks[loadString]) or ts.call("isObject", builtBricks[loadString]) == "0" then
			local brickId = createBrickFromLoadString(loadString)
			builtBricks[loadString] = brickId
		end
		coroutine.yield(1)
	end
end

local function updateMazeFromBase(baseName, seed, cellSizeTUStr, connectorUiName)
	clearShapes()
	
	local allBricks = getAllBricks()
	
	local base_bricks = {}
	for _, brick in ipairs(allBricks) do
		if brick.ntName and brick.ntName:find("^"..baseName) then
			base = brick
			break
		end
	end
	if not base then
		talk("Error: No baseplate named "..baseName)
		return
	end
	
	local cellSizeTU = vector(vecfromstr(cellSizeTUStr))
	assert(cellSizeTU)
	assert(connectorUiName)
	
	local allTemplates = extractTemplatesFromBricks(allBricks, cellSizeTU, connectorUiName)
	
	local allBrickLoadStrings = {}
	local newBrickLoadStrings = {}
	
	loadBricksOnBase(allTemplates, allBricks, base, newBrickLoadStrings, allBrickLoadStrings, seed, cellSizeTU)
	
	updateMazeBrickLoadStrings(newBrickLoadStrings)
end

local function deleteAllBuiltBricks()
	local builtBricks = MazeData.builtBricks
	for loadString, brickId in pairs(builtBricks) do
		if ts.call("isObject", brickId) == "1" then
			ts.callobj(brickId, "delete")
		end
		builtBricks[loadString] = nil
		coroutine.yield(1)
	end
end

----
-- Testing

function BuildMazeFromBase(basename, seed, cellSize, connectorUiName)
	MazeData.cor = coroutine.create(function()
		updateMazeFromBase(basename, seed, cellSize, connectorUiName)
	end)
	talk("Started building maze...")
end

local function printThreadInfo(cor)
	talk("status = "..coroutine.status(cor))
	for i = 0, 10 do talk("debug info "..i.." = ".. tostring(debug.getinfo(cor, i))) end
	for l in debug.traceback(cor):gmatch("[^\n]+") do talk(l) end
end

function MazeResume(amount)
	amount = tonumber(amount) or 1
	if MazeData.cor then
		local cor = MazeData.cor
		for i = 1, amount * (MazeData.nextTickSpeed or 1) do
			local status, data = coroutine.resume(cor)
			if not status then
				MazeData.cor = nil
				if data ~= "cannot resume dead coroutine" then talk("error: "..tostring(data)) end
				talk("Done")
				break
			else
				MazeData.nextTickSpeed = data
			end
		end
	end
end

function ClearMaze()
	MazeData.cor = coroutine.create(deleteAllBuiltBricks)
	talk("Started clearing maze...")
end
